/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.rebind;

import java.io.PrintWriter;

import org.cometmi.gwt.client.impl.ObjectWrapperClient;
import org.cometmi.gwt.client.impl.ObjectWrapperClient.MethodInvoker;
import org.cometmi.gwt.shared.MiObject;
import org.cometmi.gwt.shared.exceptions.NotAvailableConstructorException;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JAbstractMethod;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.generator.NameFactory;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

class WrapperCreator
{
	/** Simple name of class to be generated */
	private String wrapperClassName = null;

	/** Package name of class to be generated */
	private String packageName = null;

	/** The type of provided class */
	private JClassType providedType;

	private String canonicalName;

	private String staticCreatorName;

	/**
	 * Standard constructor: creates a class extending
	 * {@link ObjectWrapperClient} which will wrap an element of provided type;
	 * the provided type should implement {@link MiObject}.
	 * 
	 * @param logger
	 * @param context
	 * @param providedType The provided type implementing {@link MiObject}.
	 * @throws UnableToCompleteException
	 */
	WrapperCreator(TreeLogger logger, GeneratorContext context, JClassType providedType)
			throws UnableToCompleteException
	{
		this.providedType = providedType;

		this.wrapperClassName = this.providedType.getSimpleSourceName() + "___Wrapper";
		this.packageName = this.providedType.getPackage().getName();

		this.canonicalName = this.packageName + "." + this.wrapperClassName;

		generateClass(logger, context);

		logger.log(TreeLogger.INFO, "Generated class " + this.canonicalName);
	}

	/**
	 * Returns the name of the generated class.
	 * 
	 * @return
	 */
	String getCanonicalname()
	{
		return this.canonicalName;
	}

	/**
	 * Returns the provided type to be wrapper, implementing {@link MiObject}.
	 * 
	 * @return
	 */
	JClassType getProvideType()
	{
		return this.providedType;
	}

	/**
	 * Returns the name of the static method creating a new instance.
	 * 
	 * @return
	 */
	String getStaticCreatorMethodName()
	{
		return this.staticCreatorName;
	}

	/**
	 * Generate source code for new class. Class extends <code>HashMap</code>.
	 * 
	 * @param logger Logger object
	 * @param context Generator context
	 */
	private void generateClass(TreeLogger logger, GeneratorContext context)
	{
		TypeOracle typeOracle = context.getTypeOracle();
		PrintWriter printWriter = context.tryCreate(logger, this.packageName, this.wrapperClassName);

		/*
		 * When the print writer is null, the source code has ALREADY been
		 * generated, in such case nothing will be done.
		 */
		if (printWriter == null) return;

		/* initialize composer, set class properties, create source writer */
		ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(this.packageName,
				this.wrapperClassName);

		composer.addImport(this.providedType.getQualifiedSourceName());
		composer.addImport(MethodInvoker.class.getCanonicalName());
		composer.addImport(NotAvailableConstructorException.class.getCanonicalName());

		composer.setSuperclass(ObjectWrapperClient.class.getCanonicalName());

		SourceWriter sourceWriter = composer.createSourceWriter(context, printWriter);

		NameFactory nameFactory = new NameFactory();

		/* Generating constructor and required methods */
		generateConstructor(logger, sourceWriter, typeOracle, nameFactory);

		/*
		 * Generating a method providing an instance created by the requested
		 * constructor.
		 */
		generateStaticCreator(logger, sourceWriter, typeOracle, nameFactory);

		/* close generated class */
		sourceWriter.outdent();
		sourceWriter.println("}");

		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor.
	 * 
	 * @param logger
	 * @param w Source writer to output source code.
	 * @param typeOracle
	 * @param nameFactory
	 */
	private void generateConstructor(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		w.println("public " + this.wrapperClassName + "() { ");
		w.indent();
		w.println("super();");

		addMethodInvokers(logger, w, typeOracle, nameFactory);

		w.outdent();
		w.println("}");
	}

	/**
	 * Creates a static method returning an instance created using the provided
	 * constructor.
	 * 
	 * @param logger
	 * @param w
	 * @param typeOracle
	 * @param nameFactory
	 */
	private void generateStaticCreator(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		w.println("public static %s %s(String signature, Object[] args) throws Exception { ",
				this.providedType.getSimpleSourceName(), (this.staticCreatorName = nameFactory.createName("___create")));
		w.indent();

		String errorMessage;

		if (this.providedType.isMemberType())
			errorMessage = "because it's an inner class.";
		else if (this.providedType.isInterface() != null)
			errorMessage = "because it's an interface.";
		else if (this.providedType.isAbstract())
			errorMessage = "because it's an abstract class.";
		else
		{
			for (JConstructor constructor : this.providedType.getConstructors())
			{
				StringBuilder _signature = new StringBuilder();

				for (JType param : constructor.getParameterTypes())
					_signature.append(param.getJNISignature());

				w.print("if (signature.equals(\"%s\")) return new %s(", _signature.toString(),
						this.providedType.getSimpleSourceName());

				JParameter[] params = constructor.getParameters();

				for (int i = 0; i < params.length; i++)
				{
					if (i > 0) w.print(", ");

					JType paramType = params[i].getType();
					JPrimitiveType primitiveType = paramType.isPrimitive();

					String castTo;

					if (primitiveType != null)
						castTo = primitiveType.getQualifiedBoxedSourceName();
					else
						castTo = paramType.getParameterizedQualifiedSourceName();

					w.print("(%s)args[%d]", castTo, i);
				}

				w.println(");");
			}

			errorMessage = " because no constructor matches the provided arguments. ";
		}

		/* In case no constructor is matched, an exception will be thrown */
		w.println();
		w.println("throw new %s(\"The provided type %s can't be instantiated %s\");",
				NotAvailableConstructorException.class.getSimpleName(), this.providedType.getQualifiedSourceName(),
				errorMessage);
		w.outdent();
		w.println("}");
	}

	protected void addMethodInvokers(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, NameFactory nameFactory)
	{
		JMethod[] methods = this.providedType.getOverridableMethods();

		for (JMethod method : methods)
			if (method.isPublic())
			{
				String methodName = method.getName();
				nameFactory.addName(methodName);

				/* We don't wrap static methods, if there are. */
				if (!method.isStatic()) addMethodInvoker(logger, w, typeOracle, (JMethod) prepareMethod(method));
			}
	}

	private JAbstractMethod prepareMethod(JAbstractMethod method)
	{
		JClassType enclosingType = method.getEnclosingType();
		JParameterizedType isParameterizedType = enclosingType.isParameterized();

		if (isParameterizedType != null)
		{
			JMethod[] _methods = isParameterizedType.getMethods();

			for (int i = 0; i < _methods.length; ++i)
				if (_methods[i] == method) return isParameterizedType.getBaseType().getMethods()[i];
		}

		return method;
	}

	private void addMethodInvoker(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, JMethod method)
	{
		w.println();

		/*
		 * Reserving the name of the method, in case that default method will
		 * not conflict with some user defined method name
		 */
		String methodName = method.getName();

		/* Extracting the arguments signature */
		JParameter[] params = method.getParameters();

		StringBuilder signature = new StringBuilder(methodName);

		for (int i = 0; i < params.length; ++i)
			signature.append(params[i].getType().getJNISignature());

		logger.log(TreeLogger.INFO, "Adding invoker for method identifier: " + signature);

		w.print("methodMap.put(\"%s\", ", signature);
		w.println("new %s(){", MethodInvoker.class.getSimpleName());
		w.indent();

		JType returnType = method.getReturnType();
		JPrimitiveType returnPrmType = returnType.isPrimitive();

		String returnTypeName;

		if (returnPrmType != null)
			returnTypeName = returnPrmType.getQualifiedBoxedSourceName();
		else
			returnTypeName = returnType.getParameterizedQualifiedSourceName();

		w.println("public %s invoke(Object[] args) throws Exception {", returnTypeName);
		w.indent();

		boolean returnsVoid = (returnPrmType == JPrimitiveType.VOID);

		if (!returnsVoid) w.print("return ");

		w.print("((%s)%s.super.instance).%s(", this.providedType.getSimpleSourceName(), this.wrapperClassName,
				method.getName());

		for (int i = 0; i < params.length; ++i)
		{
			if (i > 0) w.print(", ");

			JType paramType = params[i].getType();
			JPrimitiveType primitiveType = paramType.isPrimitive();

			String castTo;

			if (primitiveType != null)
				castTo = primitiveType.getQualifiedBoxedSourceName();
			else
				castTo = paramType.getParameterizedQualifiedSourceName();

			w.print("(%s)args[%d]", castTo, i);
		}

		w.println(");");

		if (returnsVoid) w.println("return null;");

		w.outdent();
		w.println("}");

		w.outdent();
		w.println("});");
	}
}
